package net.oschina.allchat.message.processing.json;

import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

import net.oschina.allchat.message.processing.Attribute;
import net.oschina.allchat.message.processing.BadMessageException;
import net.oschina.allchat.message.processing.IElementParser;
import net.oschina.allchat.message.processing.IParser;
import net.oschina.allchat.message.processing.IParsingContext;
import net.oschina.allchat.message.processing.IProcessingDescriptor;
import net.oschina.allchat.message.processing.IProcessingFactory;
import net.oschina.allchat.message.processing.ParsingContext;
import net.oschina.allchat.message.processing.ProcessingDescriptor;
import net.oschina.allchat.message.processing.json.parsers.core.StanzaParser;
import net.oschina.allchat.message.processing.json.parsers.error.StanzaErrorParser;
import net.oschina.allchat.protocol.Constants;
import net.oschina.allchat.protocol.core.Protocol;
import net.oschina.allchat.protocol.core.ProtocolChain;
import net.oschina.allchat.protocol.core.ProtocolException;
import net.oschina.allchat.protocol.core.stanza.Stanza;
import net.oschina.allchat.protocol.core.stanza.error.BadRequest;
import net.oschina.allchat.protocol.core.stanza.error.StanzaError;
import net.oschina.allchat.protocol.core.stream.error.BadFormat;

import com.fasterxml.jackson.core.JsonFactory;
import com.fasterxml.jackson.core.JsonParseException;
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.core.JsonParser.NumberType;
import com.fasterxml.jackson.core.JsonToken;

public class ProcessingFactory implements IProcessingFactory {
	private Map<ProtocolChain, IProcessingDescriptor<?>> descriptors;
	private Map<ProtocolChain, IParser<?>> parsersCache;
	private Map<ProtocolChain, Map<String, IElementParser<?>>> elementParsersCache;
	private JsonFactory jsonFactory;
	
	public ProcessingFactory() {
		descriptors = new ConcurrentHashMap<ProtocolChain, IProcessingDescriptor<?>>();
		parsersCache = new ConcurrentHashMap<ProtocolChain, IParser<?>>();
		elementParsersCache = new ConcurrentHashMap<ProtocolChain, Map<String, IElementParser<?>>>();
		
		jsonFactory = new JsonFactory();
		jsonFactory.enable(JsonParser.Feature.ALLOW_UNQUOTED_FIELD_NAMES);
	}
	
	@Override
	public Object parse(String message) {
		return parse(message, false);
	}

	@Override
	public Object parse(String message, boolean stream) {
		try {
			return doParse(message);
		} catch (JsonParseException e) {
			throw newBadMessageException(e, stream);
		} catch (BadMessageException e) {
			throw newBadMessageException(e, stream);
		} catch (IOException e) {
			throw newInternalServerErrorException(e, stream);
		} catch (ProtocolException e) {
			throw e;
		} catch (RuntimeException e) {
			throw newInternalServerErrorException(e, stream);
		}
	}

	private ProtocolException newBadMessageException(Exception e, boolean stream) {
		if (stream) {
			return new ProtocolException(new BadFormat(e.getMessage()));
		} else {
			return new ProtocolException(new BadRequest(e.getMessage()));
		}
	}
	
	private ProtocolException newInternalServerErrorException(Exception e, boolean stream) {
		if (stream) {
			return new ProtocolException(new net.oschina.allchat.protocol.core.stream.error.InternalServerError(e.getMessage()));
		} else {
			return new ProtocolException(new net.oschina.allchat.protocol.core.stanza.error.InternalServerError(e.getMessage()));
		}
	}

	private Object doParse(String message) throws IOException, JsonParseException {
		JsonParser jsonParser = null;
		try {
			jsonParser = jsonFactory.createParser(message);
			if (jsonParser.nextToken() != JsonToken.START_OBJECT) {
				throw new BadMessageException("not a valid json document");
			}
			
			Object obj = parseProtocol(jsonParser, message);
			
			if (jsonParser.nextToken() != JsonToken.END_OBJECT) {
				throw new BadMessageException("not a valid json document");
			}
			
			if (jsonParser.nextToken() != null) {
				throw new BadMessageException("end of document expected");
			}
			
			return obj;
		} finally {
			if (jsonParser != null) {
				try {
					jsonParser.close();
				} catch (IOException e) {
					// ignore
				}
			}
		}
	}

	private Object parseProtocol(JsonParser jsonParser, String message) throws JsonParseException, IOException {
		if (jsonParser.nextToken() == JsonToken.FIELD_NAME) {
			String name = jsonParser.getCurrentName();
			if (name.startsWith(Constants.PREFIX_PROTOCOL_OBJECT)) {
				if (jsonParser.nextToken() != JsonToken.START_OBJECT) {
					throw ParsingUtils.newParsingException(jsonParser, "root element should be a object");
				}
				
				Object object = parseProtocol(jsonParser, (IParsingContext<?>)null, getProtocol(jsonParser));
				
				if (object instanceof Stanza) {
					((Stanza)object).setOriginalMessage(message);
				}
				
				return object;
			}
		}
		
		throw new BadMessageException("root protocol object not found");
	}

	private <T> Object parseProtocol(JsonParser jsonParser, IParsingContext<?> parentContext, Protocol protocol)
			throws JsonParseException, IOException {
		IParser<T> parser = null;
		ProtocolChain real = createProtocolChain(parentContext, protocol);
		
		if (descriptors.containsKey(real)) {
			parser = getParser(real);
		} else {
			ProtocolChain wildcardMatch = createProtocolChain(parentContext, new Protocol("*", protocol.getNamespace()));
			
			if (!descriptors.containsKey(wildcardMatch)) {
				throw new BadMessageException(String.format("unspported protocol. %s",
						parentContext.getProtocolChain().next(protocol)));
			}
			
			parser = getParser(wildcardMatch);
		}
		
		ParsingContext<T> context = new ParsingContext<T>(real, parser.createObject());
		
		StanzaError error = parseElement(jsonParser, parser, context);
		if (error != null) {
			return error;
		} else {
			return context.getObject();
		}
	}
	
	private Protocol getProtocol(JsonParser jsonParser) throws JsonParseException, IOException {
		String name = jsonParser.getCurrentName();
		
		if (!name.startsWith(Constants.PREFIX_PROTOCOL_OBJECT)) {
			throw new BadMessageException(String.format("%s not a protocol object", name));
		}
		
		String localName = null, namespace = null;
		int namespaceTokenIndex = name.indexOf(Constants.TOKEN_NAMESPACE);
		if (namespaceTokenIndex == 0 || namespaceTokenIndex == name.length() - 1) {
			throw new BadMessageException(String.format("%s isn't a valid protocol name", name));
		}
		
		if (namespaceTokenIndex == -1) {
			localName = name.substring(1, name.length());
		} else {
			localName = name.substring(1, namespaceTokenIndex);
			namespace = name.substring(namespaceTokenIndex + 1, name.length());
		}
		
		return new Protocol(localName, namespace);
	}
	
	private enum NextObject {
		PROTOCOL_OBJECT,
		TEXT_FIELD,
		TEXT_ONLY_OBJECT,
		OBJECT,
		ARRAY,
		ATTRIBUTE,
		UNKNOWN
	}
	
	private <T> StanzaError parseElement(JsonParser jsonParser, IParser<T> parser, ParsingContext<T> context)
			throws IOException, JsonParseException {		
		IElementParser<T> elementParser = getElementParser(parser, context);
		if (elementParser == null) {
			throw new BadMessageException(String.format("element %s not allowed be here: %s",
					context.getParsingPath(), context));
		}
		
		List<Attribute<?>> attributes = new ArrayList<Attribute<?>>();
		String text = null;
		NextObject nextObject = NextObject.UNKNOWN;
		boolean attributesProcessed = false;
		boolean textProcessed = false;
		while (jsonParser.nextToken() != JsonToken.END_OBJECT) {
			if (jsonParser.getCurrentToken() != JsonToken.FIELD_NAME) {
				throw ParsingUtils.newParsingException(jsonParser, String.format(
						"field name should be here: %s", context));
			}
			
			String fieldName = jsonParser.getCurrentName();
			JsonToken token = jsonParser.nextToken();
			if (token == JsonToken.START_OBJECT && fieldName.startsWith(Constants.PREFIX_PROTOCOL_OBJECT)) {
				nextObject = NextObject.PROTOCOL_OBJECT;
			} else if (token == JsonToken.VALUE_STRING && Constants.SYMBOL_FIELD_TEXT.equals(fieldName)) {
				if (textProcessed)
					throw ParsingUtils.newParsingException(jsonParser, String.format(
							"only one text field allowed here: %s", context));
				
				nextObject = NextObject.TEXT_FIELD;
			} else if (token == JsonToken.VALUE_STRING && fieldName.endsWith(Constants.SYMBOL_FIELD_TEXT)) {
				nextObject = NextObject.TEXT_ONLY_OBJECT;
			} else if (token == JsonToken.VALUE_FALSE ||
					token == JsonToken.VALUE_TRUE ||
					token == JsonToken.VALUE_STRING ||
					token == JsonToken.VALUE_NUMBER_FLOAT ||
					token == JsonToken.VALUE_NUMBER_INT) {
				nextObject = NextObject.ATTRIBUTE;
			} else if (token == JsonToken.START_ARRAY) {
				nextObject = NextObject.ARRAY;
			} else if (token == JsonToken.START_OBJECT) {
				nextObject = NextObject.OBJECT;
			} else {
				nextObject = NextObject.UNKNOWN;
			}
			
			if (attributesProcessed) {
				if (nextObject == NextObject.ATTRIBUTE) {
					throw ParsingUtils.newParsingException(jsonParser, String.format(
							"attributes not allowed be here: %s", context));
				}
			} else {
				// because attributes are always placed above other objects. if we find other objects,
				// we can process attributes immediately
				if (nextObject != NextObject.ATTRIBUTE && nextObject != NextObject.UNKNOWN) {
					StanzaError error = processElementAttributesAndParseError(jsonParser, context, elementParser, attributes);
					if (error != null)
						return error;
					
					attributesProcessed = true;
				}
			}
			
			switch (nextObject) {
			case PROTOCOL_OBJECT:
				Protocol protocol = getProtocol(jsonParser);
				parser.processEmbeddedObject(context, protocol, parseProtocol(jsonParser, context, protocol));
				break;
			case TEXT_FIELD:
				text = parseText(jsonParser, context, elementParser);
				textProcessed = true;
				break;
			case TEXT_ONLY_OBJECT:
				parseTextOnlyObject(jsonParser, parser, context);
				break;
			case OBJECT:
				context.enter(jsonParser.getCurrentName());
				parseElement(jsonParser, parser, context);
				context.exit();
				break;
			case ARRAY:
				parseArray(jsonParser, parser, context);
				break;
			case ATTRIBUTE:
				parseAttribute(jsonParser, attributes);
				break;
			case UNKNOWN:
			default:
				throw ParsingUtils.newParsingException(jsonParser, String.format("%s not allowed be here: %s",
						jsonParser.getCurrentToken(), context));
			}
		}
		
		if (!attributesProcessed) {
			StanzaError error = processElementAttributesAndParseError(jsonParser, context, elementParser, attributes);
			if (error != null)
				return error;
		}
		
		elementParser.processText(context, text);
		
		return null;
	}

	private <T> StanzaError processElementAttributesAndParseError(JsonParser jsonParser, ParsingContext<T> context,
			IElementParser<T> elementParser, List<Attribute<?>> attributes) throws JsonParseException, IOException {
		elementParser.processAttributes(context, attributes);
		if (context.removeAttribute(StanzaParser.KEY_ERROR) == StanzaParser.KEY_ERROR) {
			// stanza error is special, so we process it separately
			if (!(context.getObject() instanceof Stanza) && context.getObject() instanceof StanzaError) {
				throw new RuntimeException("found error. but need a Presence, Message or Iq instance");
			}
			
			StanzaErrorParser errorParser = new StanzaErrorParser();
			return errorParser.parse(this, jsonParser, (Stanza)context.getObject());
		}
		
		return null;
	}
	
	private <T> void parseArray(JsonParser jsonParser, IParser<T> parser, ParsingContext<T> context) throws JsonParseException, IOException {
		String name = jsonParser.getCurrentName();
		context.enter(name);
		
		IElementParser<T> elementParser = getElementParser(parser, context);
		
		List<Attribute<?>> attributes = Collections.emptyList();
		elementParser.processAttributes(context, attributes);
		
		jsonParser.nextToken();
		while (jsonParser.getCurrentToken() != JsonToken.END_ARRAY) {
			if (jsonParser.getCurrentToken() == JsonToken.START_OBJECT) {
				context.enter(Constants.NAME_ARRAY_ELEMENT);
				parseElement(jsonParser, parser, context);
				context.exit();
			} else {
				throw ParsingUtils.newParsingException(jsonParser, String.format("object should be here: %s", context));
			}
			
			jsonParser.nextToken();
		}
		
		elementParser.processText(context, null);
		
		context.exit();
	}
	
	private void parseAttribute(JsonParser jsonParser, List<Attribute<?>> attributes) throws JsonParseException, IOException {
		String name = jsonParser.getCurrentName();
		int prefixIndex = name.indexOf(Constants.TOKEN_NAMESPACE);
		String localName = null, prefix = null;
		if (prefixIndex != -1) {
			prefix = name.substring(0, prefixIndex);
			localName = name.substring(prefixIndex + 1, name.length());
		} else {
			localName = name;
		}
		
		JsonToken token = jsonParser.getCurrentToken();
		if (token == JsonToken.VALUE_STRING) {
			attributes.add(Attribute.createStringAttribute(prefix, localName, jsonParser.getValueAsString()));
		} else if (token == JsonToken.VALUE_TRUE || token == JsonToken.VALUE_FALSE) {
			attributes.add(Attribute.createBooleanAttribute(prefix, localName, jsonParser.getBooleanValue()));
		} else if (token == JsonToken.VALUE_NUMBER_INT || token == JsonToken.VALUE_NUMBER_FLOAT) {
			attributes.add(readNumberAttribute(prefix, localName, jsonParser));
		} else {
			throw new BadMessageException(String.format("unknown data type %s", token));
		}
	}

	private Attribute<?> readNumberAttribute(String prefix, String localName, JsonParser jsonParser) throws IOException {
		NumberType type = jsonParser.getNumberType();
		if (type == NumberType.INT) {
			return Attribute.createIntAttribute(prefix, localName, jsonParser.getValueAsInt());
		} else if (type == NumberType.LONG) {
			return Attribute.createLongAttribute(prefix, localName, jsonParser.getValueAsLong());
		} else if (type == NumberType.BIG_INTEGER) {
			return Attribute.createBigIntAttribute(prefix, localName, jsonParser.getBigIntegerValue());
		} else if (type == NumberType.FLOAT) {
			return Attribute.createFloatAttribute(prefix, localName, jsonParser.getFloatValue());
		} else if (type == NumberType.DOUBLE) {
			return Attribute.createDoubleAttribute(prefix, localName, jsonParser.getValueAsDouble());
		} else if (type == NumberType.BIG_DECIMAL) {
			return Attribute.createDecimalAttribute(prefix, localName, jsonParser.getDecimalValue());
		} else {
			throw new RuntimeException(String.format("unknown number type %s", type));
		}
	}

	private <T> String parseText(JsonParser jsonParser, ParsingContext<T> context, IElementParser<T> elementParser)
			throws IOException, JsonParseException {
		return jsonParser.getValueAsString();
	}
	
	private <T> void parseTextOnlyObject(JsonParser jsonParser, IParser<T> parser, ParsingContext<T> context)
			throws IOException, JsonParseException {
		String name = jsonParser.getCurrentName();
		name = name.substring(0, name.length() - 1); // remove '#'
		String text = jsonParser.getValueAsString();
		
		if (!context.getParsingPath().endsWith(Constants.NAME_ARRAY_ELEMENT))
			context.enter(name);
		
		IElementParser<T> elementParser = getElementParser(parser, context);
		if (elementParser == null) {
			throw new BadMessageException(String.format("element %s not allowed be here: %s",
				context.getParsingPath(), context));
		}
		
		List<Attribute<?>> attributes = Collections.emptyList();
		elementParser.processAttributes(context, attributes);
		elementParser.processText(context, text);
		
		if (!context.getParsingPath().endsWith(Constants.NAME_ARRAY_ELEMENT))
			context.exit();
	}
	
	@SuppressWarnings("unchecked")
	private <T> IElementParser<T> getElementParser(IParser<T> parser, IParsingContext<?> context) {
		Map<String, IElementParser<?>> elementParsers = elementParsersCache.get(context.getProtocolChain());
		if (elementParsers == null) {
			elementParsers = new ConcurrentHashMap<String, IElementParser<?>>();
			elementParsersCache.put(context.getProtocolChain(), elementParsers);
		}
		
		IElementParser<?> elementParser = elementParsers.get(context.getParsingPath());
		if (elementParser == null) {
			elementParser = parser.getElementParser(context.getParsingPath());
			if (elementParser != null) {
				elementParsers.put(context.getParsingPath(), elementParser);
			}
		}
		
		return (IElementParser<T>)elementParser;
	}

	@SuppressWarnings("unchecked")
	private <T> IParser<T> getParser(ProtocolChain protocolChain) {
		IParser<?> parser = parsersCache.get(protocolChain);
		if (parser == null) {
			IProcessingDescriptor<?> descriptor = descriptors.get(protocolChain);
			if (descriptor == null) {
				throw new RuntimeException(String.format("parser not found. protocol chain: %s", protocolChain));
			}
			
			parser = descriptor.getParserFactory().create();
			parsersCache.put(protocolChain, parser);
		}
		
		return (IParser<T>)parser;
	}

	private ProtocolChain createProtocolChain(IParsingContext<?> parsingContext, Protocol protocol) {
		if (parsingContext != null) {
			return parsingContext.getProtocolChain().clone().next(protocol);
		} else {
			return ProtocolChain.first(protocol);
		}
	}

	@Override
	public String translate(Object object) {
		// TODO Auto-generated method stub
		return null;
	}

	@Override
	public void register(ProtocolChain chain, ProcessingDescriptor<?> descriptor) {
		// TODO Auto-generated method stub
		descriptors.put(chain, descriptor);
	}

	@Override
	public void unregister(ProtocolChain chain) {
		// TODO Auto-generated method stub
		descriptors.remove(chain);
	}

}
